@fieldwangai/agentflow 0.1.42 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -50,6 +50,13 @@ function readUserMcpPrivateEnvObject(userId) {
50
50
  return env;
51
51
  }
52
52
 
53
+ function readUserMcpPrivateServers(userId) {
54
+ const safe = sanitizeAgentflowUserId(userId);
55
+ if (!safe) return {};
56
+ const data = readJsonObject(path.join(getAgentflowUserDataRoot(safe), "mcp-private.json"));
57
+ return data?.servers && typeof data.servers === "object" && !Array.isArray(data.servers) ? data.servers : {};
58
+ }
59
+
53
60
  function pruneCursorMcpPrivateEnvPlaceholders() {
54
61
  const filePath = path.join(os.homedir(), ".cursor", "mcp.json");
55
62
  const config = readJsonObject(filePath);
@@ -83,12 +90,98 @@ function pruneCursorMcpPrivateEnvPlaceholders() {
83
90
  fs.writeFileSync(filePath, JSON.stringify({ ...config, mcpServers: nextServers }, null, 2) + "\n", "utf-8");
84
91
  }
85
92
 
93
+ function cursorMcpServersFromFile(filePath) {
94
+ const config = readJsonObject(filePath);
95
+ return config?.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
96
+ ? config.mcpServers
97
+ : {};
98
+ }
99
+
100
+ function materializeWorkspaceCursorMcpPrivateConfig(workspaceRoot, userId) {
101
+ const safe = sanitizeAgentflowUserId(userId);
102
+ if (!safe) return () => {};
103
+ const privateServers = readUserMcpPrivateServers(safe);
104
+ if (!Object.keys(privateServers).length) return () => {};
105
+
106
+ const workspace = path.resolve(workspaceRoot || process.cwd());
107
+ const filePath = path.join(workspace, ".cursor", "mcp.json");
108
+ const globalFilePath = path.join(os.homedir(), ".cursor", "mcp.json");
109
+ const existed = fs.existsSync(filePath);
110
+ const original = existed ? fs.readFileSync(filePath, "utf-8") : "";
111
+ const config = readJsonObject(filePath);
112
+ const localServers = cursorMcpServersFromFile(filePath);
113
+ const globalServers = cursorMcpServersFromFile(globalFilePath);
114
+ const nextServers = { ...localServers };
115
+ let changed = false;
116
+
117
+ for (const [name, privateServer] of Object.entries(privateServers)) {
118
+ const current = nextServers[name] || globalServers[name];
119
+ if (!current || typeof current !== "object" || Array.isArray(current)) continue;
120
+ const privateEnv = privateServer?.env && typeof privateServer.env === "object" && !Array.isArray(privateServer.env) ? privateServer.env : {};
121
+ const privateHeaders = privateServer?.headers && typeof privateServer.headers === "object" && !Array.isArray(privateServer.headers) ? privateServer.headers : {};
122
+ if (!Object.keys(privateEnv).length && !Object.keys(privateHeaders).length) continue;
123
+
124
+ const next = { ...current };
125
+ if (Object.keys(privateEnv).length) {
126
+ const currentEnv = current.env && typeof current.env === "object" && !Array.isArray(current.env) ? current.env : {};
127
+ next.env = { ...currentEnv, ...privateEnv };
128
+ changed = true;
129
+ }
130
+ if (Object.keys(privateHeaders).length) {
131
+ const currentHeaders = current.headers && typeof current.headers === "object" && !Array.isArray(current.headers) ? current.headers : {};
132
+ next.headers = { ...currentHeaders, ...privateHeaders };
133
+ changed = true;
134
+ }
135
+ nextServers[name] = next;
136
+ }
137
+
138
+ if (!changed) return () => {};
139
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
140
+ fs.writeFileSync(filePath, JSON.stringify({ ...config, mcpServers: nextServers }, null, 2) + "\n", "utf-8");
141
+
142
+ let restored = false;
143
+ return () => {
144
+ if (restored) return;
145
+ restored = true;
146
+ try {
147
+ if (existed) fs.writeFileSync(filePath, original, "utf-8");
148
+ else if (fs.existsSync(filePath)) fs.rmSync(filePath, { force: true });
149
+ } catch {
150
+ // Best-effort restore; do not fail an already-running agent on cleanup.
151
+ }
152
+ };
153
+ }
154
+
86
155
  function agentflowUserEnv(userId) {
87
156
  const safe = sanitizeAgentflowUserId(userId);
88
157
  pruneCursorMcpPrivateEnvPlaceholders();
89
158
  return { ...readMergedEnvObject(safe), ...(safe ? readUserMcpPrivateEnvObject(safe) : {}), AGENTFLOW_USER_ID: safe };
90
159
  }
91
160
 
161
+ function runCursorAgentWithPrivateMcp(cliWorkspace, prompt, options, userId) {
162
+ const restore = materializeWorkspaceCursorMcpPrivateConfig(cliWorkspace, userId);
163
+ let handle;
164
+ try {
165
+ handle = runCursorAgentWithPrompt(cliWorkspace, prompt, options);
166
+ } catch (e) {
167
+ restore();
168
+ throw e;
169
+ }
170
+
171
+ let restored = false;
172
+ const safeRestore = () => {
173
+ if (restored) return;
174
+ restored = true;
175
+ restore();
176
+ };
177
+ if (handle?.child?.once) {
178
+ handle.child.once("exit", safeRestore);
179
+ handle.child.once("error", safeRestore);
180
+ }
181
+ const finished = Promise.resolve(handle.finished).finally(safeRestore);
182
+ return { ...handle, finished };
183
+ }
184
+
92
185
  // ─── script 内容注入辅助 ─────────────────────────────────────────────────
93
186
 
94
187
  /**
@@ -203,10 +296,10 @@ export function startComposerAgent(opts) {
203
296
  });
204
297
  }
205
298
 
206
- return runCursorAgentWithPrompt(cliWs, prompt, {
299
+ return runCursorAgentWithPrivateMcp(cliWs, prompt, {
207
300
  ...common,
208
301
  model: model || undefined,
209
- });
302
+ }, opts.agentflowUserId);
210
303
  }
211
304
 
212
305
  // ─── 为单个 agent 步骤构建 prompt ──────────────────────────────────────────
@@ -496,12 +589,12 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
496
589
  setChild(handle.child);
497
590
  await handle.finished;
498
591
  } else {
499
- const handle = runCursorAgentWithPrompt(cliWs, agentPrompt, {
592
+ const handle = runCursorAgentWithPrivateMcp(cliWs, agentPrompt, {
500
593
  onStreamEvent: stepEmit,
501
594
  model: model || undefined,
502
595
  force: Boolean(opts.force),
503
596
  env,
504
- });
597
+ }, opts.agentflowUserId || opts.flowContext?.userId);
505
598
  setChild(handle.child);
506
599
  await handle.finished;
507
600
  }
@@ -769,12 +862,12 @@ export function startComposerMultiStep(opts) {
769
862
  currentChild = handle.child;
770
863
  await handle.finished;
771
864
  } else {
772
- const handle = runCursorAgentWithPrompt(cliWs, agentPrompt, {
865
+ const handle = runCursorAgentWithPrivateMcp(cliWs, agentPrompt, {
773
866
  onStreamEvent: stepEmit,
774
867
  model: model || undefined,
775
868
  force: Boolean(opts.force),
776
869
  env,
777
- });
870
+ }, opts.agentflowUserId || opts.flowContext?.userId);
778
871
  currentChild = handle.child;
779
872
  await handle.finished;
780
873
  }
@@ -108,8 +108,13 @@ const MIME = {
108
108
  ".js": "text/javascript; charset=utf-8",
109
109
  ".css": "text/css; charset=utf-8",
110
110
  ".json": "application/json; charset=utf-8",
111
- ".ico": "image/x-icon",
111
+ ".png": "image/png",
112
+ ".jpg": "image/jpeg",
113
+ ".jpeg": "image/jpeg",
114
+ ".gif": "image/gif",
115
+ ".webp": "image/webp",
112
116
  ".svg": "image/svg+xml",
117
+ ".ico": "image/x-icon",
113
118
  };
114
119
 
115
120
  const RUN_CONFIG_FILENAME = "run-config.json";
@@ -1125,6 +1130,7 @@ const WORKSPACE_TEXT_EXTS = new Set([
1125
1130
  ".mjs",
1126
1131
  ".cjs",
1127
1132
  ]);
1133
+ const WORKSPACE_IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
1128
1134
 
1129
1135
  function resolveWorkspaceFilePath(workspaceRoot, relPath) {
1130
1136
  const root = path.resolve(workspaceRoot);
@@ -1144,9 +1150,59 @@ function workspaceFileIcon(fileName, isDir = false) {
1144
1150
  if ([".yaml", ".yml", ".json"].includes(ext)) return "data_object";
1145
1151
  if (ext === ".css") return "palette";
1146
1152
  if (ext === ".html") return "web";
1153
+ if (WORKSPACE_IMAGE_EXTS.has(ext)) return "image";
1147
1154
  return "draft";
1148
1155
  }
1149
1156
 
1157
+ function sanitizeWorkspaceUploadName(filename) {
1158
+ const parsed = path.parse(String(filename || "image").replace(/\\/g, "/").split("/").pop() || "image");
1159
+ const stem = (parsed.name || "image")
1160
+ .trim()
1161
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
1162
+ .replace(/^-+|-+$/g, "")
1163
+ .slice(0, 80) || "image";
1164
+ const ext = String(parsed.ext || "").toLowerCase();
1165
+ return `${stem}${WORKSPACE_IMAGE_EXTS.has(ext) ? ext : ".png"}`;
1166
+ }
1167
+
1168
+ function uniqueWorkspaceRelPath(workspaceRoot, relPath) {
1169
+ let { abs, rel } = resolveWorkspaceFilePath(workspaceRoot, relPath);
1170
+ if (!fs.existsSync(abs)) return { abs, rel };
1171
+ const parsed = path.parse(rel);
1172
+ for (let i = 1; i < 1000; i += 1) {
1173
+ const candidate = path.posix.join(parsed.dir, `${parsed.name}-${i}${parsed.ext}`);
1174
+ const resolved = resolveWorkspaceFilePath(workspaceRoot, candidate);
1175
+ if (!fs.existsSync(resolved.abs)) return resolved;
1176
+ }
1177
+ return { abs, rel };
1178
+ }
1179
+
1180
+ function workspaceDownloadContentDisposition(relPath) {
1181
+ const fallbackName = path.basename(String(relPath || "download")) || "download";
1182
+ const quotedName = fallbackName.replace(/[\r\n"\\]/g, "_");
1183
+ return `attachment; filename="${quotedName}"; filename*=UTF-8''${encodeURIComponent(fallbackName)}`;
1184
+ }
1185
+
1186
+ const WORKSPACE_FILE_SKIP_REL_PREFIXES = [
1187
+ ".workspace/agentflow/worktrees",
1188
+ ".workspace/agentflow/git-repos",
1189
+ ".workspace/agentflow/runBuild",
1190
+ ".workspace/agentflow/composer-logs",
1191
+ ];
1192
+
1193
+ function workspacePathInside(parent, candidate) {
1194
+ const base = path.resolve(parent);
1195
+ const target = path.resolve(candidate);
1196
+ return target === base || target.startsWith(base + path.sep);
1197
+ }
1198
+
1199
+ function shouldSkipWorkspaceFileRelPath(relPath) {
1200
+ const normalized = String(relPath || "").replace(/\\/g, "/").replace(/^\/+/, "");
1201
+ return WORKSPACE_FILE_SKIP_REL_PREFIXES.some((prefix) => (
1202
+ normalized === prefix || normalized.startsWith(`${prefix}/`)
1203
+ ));
1204
+ }
1205
+
1150
1206
  function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget = { count: 0 }) {
1151
1207
  if (depth > maxDepth || budget.count > 500) return [];
1152
1208
  let entries;
@@ -1161,6 +1217,7 @@ function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget
1161
1217
  if (entry.name.startsWith(".") && entry.name !== ".agents" && entry.name !== ".codex") continue;
1162
1218
  const abs = path.join(dir, entry.name);
1163
1219
  const rel = path.relative(root, abs).replace(/\\/g, "/");
1220
+ if (shouldSkipWorkspaceFileRelPath(rel)) continue;
1164
1221
  if (entry.isDirectory()) {
1165
1222
  if (WORKSPACE_FILE_SKIP_DIRS.has(entry.name)) continue;
1166
1223
  budget.count++;
@@ -1174,7 +1231,7 @@ function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget
1174
1231
  } else if (entry.isFile()) {
1175
1232
  if (WORKSPACE_FILE_SKIP_FILES.has(entry.name)) continue;
1176
1233
  const ext = path.extname(entry.name).toLowerCase();
1177
- if (!WORKSPACE_TEXT_EXTS.has(ext)) continue;
1234
+ if (!WORKSPACE_TEXT_EXTS.has(ext) && !WORKSPACE_IMAGE_EXTS.has(ext)) continue;
1178
1235
  let size = 0;
1179
1236
  try { size = fs.statSync(abs).size; } catch {}
1180
1237
  budget.count++;
@@ -1232,6 +1289,43 @@ function normalizeWorkspaceGraphPayload(payload) {
1232
1289
  };
1233
1290
  }
1234
1291
 
1292
+ function workspaceRunTouchedNodeIds(result) {
1293
+ const ids = new Set();
1294
+ for (const id of Array.isArray(result?.order) ? result.order : []) {
1295
+ const text = String(id || "").trim();
1296
+ if (text) ids.add(text);
1297
+ }
1298
+ for (const event of Array.isArray(result?.events) ? result.events : []) {
1299
+ const nodeId = String(event?.nodeId || "").trim();
1300
+ if (nodeId) ids.add(nodeId);
1301
+ for (const displayId of Array.isArray(event?.displayNodeIds) ? event.displayNodeIds : []) {
1302
+ const text = String(displayId || "").trim();
1303
+ if (text) ids.add(text);
1304
+ }
1305
+ }
1306
+ return ids;
1307
+ }
1308
+
1309
+ function mergeWorkspaceRunGraph(currentGraph, runGraph, touchedIds) {
1310
+ const current = normalizeWorkspaceGraphPayload(currentGraph || {});
1311
+ const run = normalizeWorkspaceGraphPayload(runGraph || {});
1312
+ const ids = touchedIds instanceof Set ? touchedIds : new Set(touchedIds || []);
1313
+ const instances = { ...(current.instances || {}) };
1314
+ for (const id of ids) {
1315
+ if (run.instances && Object.prototype.hasOwnProperty.call(run.instances, id)) {
1316
+ instances[id] = run.instances[id];
1317
+ }
1318
+ }
1319
+ return {
1320
+ ...current,
1321
+ version: 1,
1322
+ instances,
1323
+ edges: Array.isArray(current.edges) ? current.edges : [],
1324
+ ui: current.ui && typeof current.ui === "object" ? current.ui : { nodePositions: {} },
1325
+ updatedAt: new Date().toISOString(),
1326
+ };
1327
+ }
1328
+
1235
1329
  function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
1236
1330
  const flowId = params.flowId != null ? String(params.flowId).trim() : "";
1237
1331
  if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
@@ -1952,6 +2046,39 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs) {
1952
2046
  return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
1953
2047
  }
1954
2048
 
2049
+ function workspaceInputValues(graph, nodeId, outputs) {
2050
+ const values = {};
2051
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
2052
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
2053
+ const target = instances[String(nodeId || "")] || {};
2054
+ const inputSlots = Array.isArray(target.input) ? target.input : [];
2055
+ for (const edge of edges) {
2056
+ if (String(edge?.target || "") !== String(nodeId)) continue;
2057
+ const index = workspaceHandleIndex(edge?.targetHandle, "input");
2058
+ const slot = inputSlots[index] || null;
2059
+ const name = String(slot?.name || "").trim();
2060
+ if (!name || isWorkspaceSemanticInputSlot(slot)) continue;
2061
+ const value = workspaceOutputSlotValueForEdge(graph, outputs, edge);
2062
+ if (String(value || "").trim()) values[name] = String(value);
2063
+ }
2064
+ for (const slot of inputSlots) {
2065
+ const name = String(slot?.name || "").trim();
2066
+ if (!name || isWorkspaceSemanticInputSlot(slot) || Object.prototype.hasOwnProperty.call(values, name)) continue;
2067
+ const value = workspaceSlotValue(slot);
2068
+ if (String(value || "").trim()) values[name] = String(value);
2069
+ }
2070
+ return values;
2071
+ }
2072
+
2073
+ function workspaceResolveBodyPlaceholders(body, inputValues = {}) {
2074
+ const raw = String(body || "");
2075
+ if (!raw.includes("${")) return raw;
2076
+ return raw.replace(/\$\{([A-Za-z_][A-Za-z0-9_-]*)\}/g, (match, name) => {
2077
+ if (!Object.prototype.hasOwnProperty.call(inputValues, name)) return match;
2078
+ return String(inputValues[name] ?? "");
2079
+ });
2080
+ }
2081
+
1955
2082
  function parseWorkspaceSkillKeys(raw) {
1956
2083
  const text = String(raw || "").trim();
1957
2084
  if (!text) return [];
@@ -2116,9 +2243,9 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
2116
2243
  return updated;
2117
2244
  }
2118
2245
 
2119
- function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "") {
2246
+ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}) {
2120
2247
  const instance = graph.instances[nodeId] || {};
2121
- const body = String(instance.body || "").trim();
2248
+ const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
2122
2249
  const label = String(instance.label || nodeId).trim();
2123
2250
  const downstreamRequirements = workspaceDownstreamDisplayRequirements(graph, nodeId);
2124
2251
  const outputProtocolRequirements = workspaceOutputProtocolRequirements(graph, nodeId);
@@ -2136,6 +2263,72 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock
2136
2263
  ].filter(Boolean).join("\n");
2137
2264
  }
2138
2265
 
2266
+ function workspaceDefaultWorktreeRoot(scopedRoot) {
2267
+ return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "worktrees");
2268
+ }
2269
+
2270
+ function workspaceShouldAutoCleanupWorktree(scopedRoot, worktreePath, hasExplicitWorktreePath) {
2271
+ if (hasExplicitWorktreePath || !worktreePath) return false;
2272
+ return workspacePathInside(workspaceDefaultWorktreeRoot(scopedRoot), worktreePath);
2273
+ }
2274
+
2275
+ function workspaceTrackAutoCleanupWorktree(list, item) {
2276
+ const rawTarget = String(item?.worktreePath || "").trim();
2277
+ if (!rawTarget) return;
2278
+ const target = path.resolve(rawTarget);
2279
+ if (list.some((entry) => path.resolve(entry.worktreePath) === target)) return;
2280
+ list.push({ ...item, worktreePath: target });
2281
+ }
2282
+
2283
+ function workspaceUntrackAutoCleanupWorktree(list, worktreePath) {
2284
+ const rawTarget = String(worktreePath || "").trim();
2285
+ if (!rawTarget) return;
2286
+ const target = path.resolve(rawTarget);
2287
+ for (let i = list.length - 1; i >= 0; i -= 1) {
2288
+ if (path.resolve(list[i].worktreePath) === target) list.splice(i, 1);
2289
+ }
2290
+ }
2291
+
2292
+ function workspaceMarkAutoWorktreeCleaned(graph, entry) {
2293
+ const instance = graph?.instances?.[entry.nodeId];
2294
+ if (!instance) return false;
2295
+ let nextInstance = workspaceSetOutputSlot(instance, "worktreePath", "");
2296
+ nextInstance = workspaceSetOutputSlot(nextInstance, "gitContext", "");
2297
+ nextInstance = workspaceSetOutputSlot(nextInstance, "workspaceContext", "");
2298
+ graph.instances[entry.nodeId] = nextInstance;
2299
+ return true;
2300
+ }
2301
+
2302
+ function workspaceCleanupAutoWorktrees(list, graph, emit) {
2303
+ for (const entry of [...list].reverse()) {
2304
+ try {
2305
+ const result = unloadGitWorktree({
2306
+ repoPath: entry.repoPath,
2307
+ worktreePath: entry.worktreePath,
2308
+ force: false,
2309
+ prune: true,
2310
+ });
2311
+ emit({
2312
+ type: "natural",
2313
+ kind: "status",
2314
+ nodeId: entry.nodeId,
2315
+ text: `已清理临时 worktree:${result.worktreePath}`,
2316
+ });
2317
+ if (workspaceMarkAutoWorktreeCleaned(graph, entry)) {
2318
+ emit({ type: "graph", nodeId: entry.nodeId, graph });
2319
+ }
2320
+ } catch (e) {
2321
+ emit({
2322
+ type: "natural",
2323
+ kind: "warning",
2324
+ nodeId: entry.nodeId,
2325
+ text: `临时 worktree 未自动清理:${entry.worktreePath}\n原因:${e?.message || String(e)}`,
2326
+ });
2327
+ }
2328
+ }
2329
+ list.splice(0, list.length);
2330
+ }
2331
+
2139
2332
  async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
2140
2333
  const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
2141
2334
  const runNodeId = String(payload?.runNodeId || "").trim();
@@ -2193,7 +2386,9 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2193
2386
  };
2194
2387
  let cwd = scopedRoot;
2195
2388
  const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
2389
+ const autoCleanupWorktrees = [];
2196
2390
 
2391
+ try {
2197
2392
  for (const nodeId of order) {
2198
2393
  throwIfAborted();
2199
2394
  const instance = graph.instances[nodeId];
@@ -2378,13 +2573,23 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2378
2573
  (gitContext?.repoPath ? path.resolve(gitContext.repoPath) : "");
2379
2574
  if (!repoPath) throw new Error("Load Worktree requires repoPath");
2380
2575
  const branch = workspaceSlotValue(workspaceSlotByName(instance, "branch")).trim();
2381
- const rawWorktreePath = workspaceSlotValue(workspaceSlotByName(instance, "worktreePath")).trim();
2576
+ const worktreeInputSlot = (Array.isArray(instance.input) ? instance.input : [])
2577
+ .find((slot) => String(slot?.name || "") === "worktreePath") || null;
2578
+ const rawWorktreePath = workspaceSlotValue(worktreeInputSlot || workspaceSlotByName(instance, "worktreePath")).trim();
2382
2579
  const worktreePath = rawWorktreePath ? workspaceResolvePath(cwd, rawWorktreePath) : (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : "");
2580
+ const hasExplicitWorktreePath = Boolean(rawWorktreePath) || Boolean(gitContext?.worktreePath);
2383
2581
  const previousCwd = cwd;
2384
2582
  const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
2385
2583
  const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
2386
2584
  const pruneMissing = pruneMissingRaw !== "false";
2387
2585
  const result = loadGitWorktree({ repoPath, branch, worktreePath, pipelineWorkspace: scopedRoot, force, pruneMissing });
2586
+ if (workspaceShouldAutoCleanupWorktree(scopedRoot, result.worktreePath, hasExplicitWorktreePath)) {
2587
+ workspaceTrackAutoCleanupWorktree(autoCleanupWorktrees, {
2588
+ nodeId,
2589
+ repoPath: result.repoRoot,
2590
+ worktreePath: result.worktreePath,
2591
+ });
2592
+ }
2388
2593
  const outGitContext = buildGitContext({
2389
2594
  repoPath: result.repoRoot,
2390
2595
  worktreePath: result.worktreePath,
@@ -2427,6 +2632,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2427
2632
  const pruneRaw = workspaceSlotValue(workspaceSlotByName(instance, "prune")).trim().toLowerCase();
2428
2633
  const prune = pruneRaw !== "false";
2429
2634
  const result = unloadGitWorktree({ repoPath, worktreePath, force, prune });
2635
+ workspaceUntrackAutoCleanupWorktree(autoCleanupWorktrees, result.worktreePath);
2430
2636
  const previousContext = workspaceContext?.previous && typeof workspaceContext.previous === "object" ? workspaceContext.previous : null;
2431
2637
  cwd = previousContext?.cwd ? path.resolve(String(previousContext.cwd)) : scopedRoot;
2432
2638
  let nextInstance = workspaceSetOutputSlot(instance, "removed", "true");
@@ -2483,14 +2689,15 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2483
2689
 
2484
2690
  const prepareStartedAt = Date.now();
2485
2691
  const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs);
2486
- const body = String(instance.body || "").trim();
2692
+ const inputValues = workspaceInputValues(graph, nodeId, outputs);
2693
+ const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
2487
2694
  if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
2488
2695
  throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
2489
2696
  }
2490
2697
  const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
2491
2698
  const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks, upstreamSkillBlocks ? "" : loadSkillsBlockForKeys(fallbackSelectedSkillKeys));
2492
2699
  const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
2493
- const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock);
2700
+ const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock, inputValues);
2494
2701
  emitTiming(nodeId, "prepare-agent-prompt", prepareStartedAt, { promptChars: prompt.length, upstreamChars: String(upstreamText || "").length, skillsChars: promptSkillsBlock.length, mcpChars: promptMcpBlock.length });
2495
2702
  emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
2496
2703
  let content = "";
@@ -2551,6 +2758,9 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
2551
2758
  if (slotUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
2552
2759
  emit({ type: "node-done", nodeId, definitionId: defId });
2553
2760
  }
2761
+ } finally {
2762
+ workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
2763
+ }
2554
2764
  if (pauseNodeIds.length > 0) {
2555
2765
  emit({ type: "paused", nodeIds: pauseNodeIds, message: `Workspace run paused at ${pauseNodeIds.join(", ")}` });
2556
2766
  }
@@ -2655,6 +2865,47 @@ function parseFlowsImportForm(req) {
2655
2865
  });
2656
2866
  }
2657
2867
 
2868
+ function parseWorkspaceUploadForm(req) {
2869
+ return new Promise((resolve, reject) => {
2870
+ const bb = busboy({
2871
+ headers: req.headers,
2872
+ limits: { files: 1, fileSize: 10 * 1024 * 1024, parts: 32 },
2873
+ });
2874
+ const fields = {};
2875
+ const chunks = [];
2876
+ let filename = "";
2877
+ let mimeType = "";
2878
+ let gotFile = false;
2879
+ bb.on("field", (name, val) => {
2880
+ fields[String(name || "")] = String(val || "");
2881
+ });
2882
+ bb.on("file", (name, file, info) => {
2883
+ if (name !== "file") {
2884
+ file.resume();
2885
+ return;
2886
+ }
2887
+ gotFile = true;
2888
+ filename = info.filename || "";
2889
+ mimeType = info.mimeType || "";
2890
+ file.on("data", (d) => chunks.push(d));
2891
+ file.on("limit", () => {
2892
+ reject(new Error("FILE_TOO_LARGE"));
2893
+ });
2894
+ });
2895
+ bb.on("finish", () => {
2896
+ resolve({
2897
+ fields,
2898
+ file: Buffer.concat(chunks),
2899
+ filename,
2900
+ mimeType,
2901
+ gotFile,
2902
+ });
2903
+ });
2904
+ bb.on("error", reject);
2905
+ req.pipe(bb);
2906
+ });
2907
+ }
2908
+
2658
2909
  /** GET 读 flow / nodes / SSE 等 */
2659
2910
  function isValidFlowSourceRead(s) {
2660
2911
  return s === "builtin" || s === "admin" || s === "user" || s === "workspace";
@@ -3331,8 +3582,11 @@ export function startUiServer({
3331
3582
  signal: controller.signal,
3332
3583
  onActiveChild: setActiveChild,
3333
3584
  });
3334
- fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
3335
- writeEvent({ type: "done", ok: true, path: graphPath, graph: result.graph, order: result.order, pauseNodeIds: result.pauseNodeIds || [] });
3585
+ const currentGraph = readWorkspaceGraph(scoped.root).graph;
3586
+ const touchedIds = workspaceRunTouchedNodeIds(result);
3587
+ const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
3588
+ fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
3589
+ writeEvent({ type: "done", ok: true, path: graphPath, graph: mergedGraph, order: result.order, touchedNodeIds: Array.from(touchedIds), pauseNodeIds: result.pauseNodeIds || [] });
3336
3590
  res.end();
3337
3591
  } catch (e) {
3338
3592
  if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
@@ -3352,8 +3606,11 @@ export function startUiServer({
3352
3606
  onActiveChild: setActiveChild,
3353
3607
  });
3354
3608
  const graphPath = workspaceGraphPath(scoped.root);
3355
- fs.writeFileSync(graphPath, JSON.stringify(result.graph, null, 2) + "\n", "utf-8");
3356
- json(res, 200, { ok: true, path: graphPath, ...result });
3609
+ const currentGraph = readWorkspaceGraph(scoped.root).graph;
3610
+ const touchedIds = workspaceRunTouchedNodeIds(result);
3611
+ const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
3612
+ fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
3613
+ json(res, 200, { ok: true, path: graphPath, ...result, graph: mergedGraph, touchedNodeIds: Array.from(touchedIds) });
3357
3614
  } catch (e) {
3358
3615
  if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
3359
3616
  json(res, 200, { ok: false, stopped: true, message: "Workspace run stopped" });
@@ -3422,6 +3679,41 @@ export function startUiServer({
3422
3679
  return;
3423
3680
  }
3424
3681
 
3682
+ if (req.method === "GET" && url.pathname === "/api/workspace/file/raw") {
3683
+ try {
3684
+ const scoped = resolveWorkspaceScopeRoot(root, {
3685
+ flowId: url.searchParams.get("flowId") || "",
3686
+ flowSource: url.searchParams.get("flowSource") || "user",
3687
+ archived: url.searchParams.get("archived") === "1",
3688
+ }, userCtx);
3689
+ if (scoped.error) {
3690
+ json(res, 400, { error: scoped.error });
3691
+ return;
3692
+ }
3693
+ const { abs, rel } = resolveWorkspaceFilePath(scoped.root, url.searchParams.get("path") || "");
3694
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
3695
+ json(res, 404, { error: "File not found" });
3696
+ return;
3697
+ }
3698
+ const ext = path.extname(abs).toLowerCase();
3699
+ const type = MIME[ext] || "application/octet-stream";
3700
+ const data = fs.readFileSync(abs);
3701
+ const headers = {
3702
+ "Content-Type": type,
3703
+ "Content-Length": data.length,
3704
+ "Cache-Control": "no-store",
3705
+ };
3706
+ if (url.searchParams.get("download") === "1") {
3707
+ headers["Content-Disposition"] = workspaceDownloadContentDisposition(rel);
3708
+ }
3709
+ res.writeHead(200, headers);
3710
+ res.end(data);
3711
+ } catch (e) {
3712
+ json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
3713
+ }
3714
+ return;
3715
+ }
3716
+
3425
3717
  if (req.method === "POST" && url.pathname === "/api/workspace/file") {
3426
3718
  let payload;
3427
3719
  try {
@@ -3458,6 +3750,54 @@ export function startUiServer({
3458
3750
  return;
3459
3751
  }
3460
3752
 
3753
+ if (req.method === "POST" && url.pathname === "/api/workspace/upload") {
3754
+ let parsed;
3755
+ try {
3756
+ parsed = await parseWorkspaceUploadForm(req);
3757
+ } catch (e) {
3758
+ json(res, /FILE_TOO_LARGE/.test(String(e.message || e)) ? 413 : 400, { error: (e && e.message) || String(e) });
3759
+ return;
3760
+ }
3761
+ try {
3762
+ if (!parsed.gotFile || !parsed.file.length) {
3763
+ json(res, 400, { error: "Missing upload file" });
3764
+ return;
3765
+ }
3766
+ const scoped = resolveWorkspaceScopeRoot(root, {
3767
+ flowId: parsed.fields.flowId || "",
3768
+ flowSource: parsed.fields.flowSource || "user",
3769
+ archived: parsed.fields.archived === "1" || parsed.fields.archived === "true" || parsed.fields.flowArchived === "true",
3770
+ }, userCtx);
3771
+ if (scoped.error) {
3772
+ json(res, 400, { error: scoped.error });
3773
+ return;
3774
+ }
3775
+ if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
3776
+ json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
3777
+ return;
3778
+ }
3779
+ const safeName = sanitizeWorkspaceUploadName(parsed.filename);
3780
+ const ext = path.extname(safeName).toLowerCase();
3781
+ if (!WORKSPACE_IMAGE_EXTS.has(ext) || (parsed.mimeType && !/^image\//i.test(parsed.mimeType))) {
3782
+ json(res, 400, { error: "Only image uploads are supported" });
3783
+ return;
3784
+ }
3785
+ const targetDir = String(parsed.fields.dir || "img").trim().replace(/^[/\\]+/, "") || "img";
3786
+ const target = uniqueWorkspaceRelPath(scoped.root, path.posix.join(targetDir.replace(/\\/g, "/"), safeName));
3787
+ fs.mkdirSync(path.dirname(target.abs), { recursive: true });
3788
+ fs.writeFileSync(target.abs, parsed.file);
3789
+ json(res, 200, {
3790
+ ok: true,
3791
+ path: target.rel,
3792
+ size: parsed.file.length,
3793
+ mimeType: parsed.mimeType,
3794
+ });
3795
+ } catch (e) {
3796
+ json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
3797
+ }
3798
+ return;
3799
+ }
3800
+
3461
3801
  if (req.method === "POST" && url.pathname === "/api/workspace/folder") {
3462
3802
  let payload;
3463
3803
  try {
@@ -15,6 +15,7 @@ output:
15
15
  - type: text
16
16
  name: content
17
17
  default: ""
18
+ showOnNode: true
18
19
  - type: node
19
20
  name: next
20
21
  default: ""
@@ -23,7 +23,7 @@ output:
23
23
  - type: text
24
24
  name: content
25
25
  default: ""
26
- showOnNode: false
26
+ showOnNode: true
27
27
  - type: node
28
28
  name: next
29
29
  default: ""